package me.dwk.dandan.function.reflect;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import lombok.extern.slf4j.Slf4j;
import me.dwk.dandan.function.Function;

/**
 * This class finds classes that extend one of a set of parent classes
 */
@Slf4j
public final class ClassFinder {

	private static final String	DOT_JAR			= ".jar";				// $NON-NLS-1$
	private static final String	DOT_CLASS		= ".class";				// $NON-NLS-1$
	private static final int	DOT_CLASS_LEN	= DOT_CLASS.length();

	private ClassFinder() {}

	public static List<String> findClassesThatExtend(Class<?>[] superClasses) throws IOException {
		return findClassesThatExtend(superClasses, false);
	}

	public static List<String> findClassesThatExtend(	final Class<?>[] classNames,
														final boolean innerClasses) throws IOException {
		return findClasses(new ExtendsClassFilter(classNames, innerClasses));
	}

	public static List<String> findClasses(ExtendsClassFilter filter) throws IOException {

		// Find all jars in the classpath path
		final String classpath = System.getProperty("java.class.path");

		StringTokenizer tok = new StringTokenizer(classpath, File.pathSeparator);

		Set<String> listClasses = new TreeSet<>();

		while (tok.hasMoreTokens()) {
			String nextToken = tok.nextToken();
			log.info("checking:" + nextToken);
			try {
				findClassesInOnePath(new URL(nextToken).toURI(), listClasses, filter);
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
		// first get all the classes
		log.info("listClasses.size()={}", listClasses.size());
		for (String clazz : listClasses) {
			log.info("listClasses : {}", clazz);
		}

		return new ArrayList<>(listClasses);
	}

	/**
	 * Converts a class file from the text stored in a Jar file to a version that
	 * can be used in Class.forName().
	 *
	 * @param strClassName
	 *            the class name from a Jar file
	 * @return String the Java-style dotted version of the name
	 */
	private static String fixClassName(String strClassName) {
		String fixedClassName = strClassName.replace('\\', '.'); // $NON-NLS-1$ // $NON-NLS-2$
		fixedClassName = fixedClassName.replace('/', '.'); // $NON-NLS-1$ // $NON-NLS-2$
		// remove ".class"
		fixedClassName = fixedClassName.substring(0, fixedClassName.length() - DOT_CLASS_LEN);
		return fixedClassName;
	}

	public static void main(String[] args) {
		Set<String> listClasses = new TreeSet<>();
		try {
			URI uri = new File("D:\\eclipse-workspaces\\jmeter\\ApiModularPlat\\target\\ApiModularPlat-0.0.1-SNAPSHOT.war").toURI();
			findClassesInOnePath(uri, listClasses, new ExtendsClassFilter(new Class[] {
					Function.class }, false));
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println(listClasses);
	}

	// private static final String PROTOCOL_JAR = "jar";
	private static final String JAR_URL_SEPERATOR = "!/";

	private static void findClassesInOnePath(	URI strPath,
												Set<String> listClasses,
												ExtendsClassFilter filter) throws IOException {
		File file = new File(strPath);
		if (file.isDirectory()) {
			findClassesInPathsDir(strPath, file, listClasses, filter);
		} else {
			try (ZipFile zipFile = new ZipFile(file)) {
				Enumeration<? extends ZipEntry> entries = zipFile.entries();
				while (entries.hasMoreElements()) {
					ZipEntry nextElement = entries.nextElement();
					String strEntry = nextElement.toString();
					System.out.println(strEntry);
					if (strEntry.endsWith(DOT_CLASS)) {
						String fixedClassName = fixClassName(strEntry);
						applyFiltering(listClasses, filter, fixedClassName);
					} else if (strEntry.endsWith(DOT_JAR)) {
						try {
							URI jarFile = new URL(strPath.toURL()
														+ JAR_URL_SEPERATOR
														+ strEntry).toURI();

							findClassesInOnePath(jarFile, listClasses, filter);
						} catch (Exception e) {
							e.printStackTrace();
						}

					}
				}
			} catch (IOException e) {
				System.err.println(e);
				log.warn("Can not open the jar {}, message: {}", strPath, e.getLocalizedMessage(), e);
			}
		}

	}

	private static void findClassesInPathsDir(	URI strPath,
												File dir,
												Set<String> listClasses,
												ExtendsClassFilter filter) throws IOException {
		String[] list = dir.list();
		if (list == null) {
			log.warn("{} is not a folder", dir.getAbsolutePath());
			return;
		}

		for (String filePath : list) {
			File file = new File(dir, filePath);
			if (file.isDirectory()) {
				// Recursive call
				findClassesInPathsDir(strPath, file, listClasses, filter);
			} else if (filePath.endsWith(DOT_CLASS) && file.exists() && (file.length() != 0)) {
				final String path = file.getPath();
				String className = path.substring(strPath.getPath().length() + 1, path.lastIndexOf('.')) // $NON-NLS-1$
						.replace(File.separator.charAt(0), '.');// $NON-NLS-1$
				applyFiltering(listClasses, filter, className);
			}
		}
	}

	/**
	 * Run {@link ClassFilter#accept(String)} on className and add to listClasses if
	 * accept returns true In case of Throwable, className will not be added
	 *
	 * @param classesSet
	 *            Set of class names
	 * @param filter
	 *            {@link ClassFilter}
	 * @param className
	 *            Full class name
	 */
	private static void applyFiltering(	Set<String> classesSet,
										ExtendsClassFilter filter,
										String className) {
		try {
			if (filter.accept(className)) {
				classesSet.add(className);
			}
		} catch (Throwable e) { // NOSONAR : We need to trap also Errors
			log.error("Error filtering class {}, it will be ignored", className, e);
		}
	}

}
